package part9.interpreter;

import part9.ast.*;
import token.Token;

import java.util.ArrayList;

public class Parser {
    private final Lexer lexer;
    private Token currentToken;

    public Parser(Lexer lexer) throws Exception {
        this.lexer = lexer;
        this.currentToken = lexer.getNextToken();
    }

    private void error() throws Exception {
        throw new Exception("Invalid syntax");
    }

    private void eat(Token.TokenType tokenType) throws Exception {
        if (currentToken.type == tokenType){
            currentToken = lexer.getNextToken();
        }
        else {
            this.error();
        }
    }

    private AST program() throws Exception {
        // program : compound_statement DOT
        AST node = compound_statement();
        eat(Token.TokenType.DOT);
        return node;
    }

    private AST compound_statement() throws Exception {
        // compound_statement: BEGIN statement_list END
        eat(Token.TokenType.BEGIN);
        ArrayList<AST> nodes = statementList();
        eat(Token.TokenType.END);

        Compound root = new Compound();
        root.children = nodes;
        return root;
    }

    private ArrayList<AST> statementList() throws Exception {
        /*
        statement_list : statement
                       | statement SEMI statement_list
         */
        AST node = statement();
        ArrayList<AST> results = new ArrayList<>();
        results.add(node);

        while (currentToken.type == Token.TokenType.SEMI) {
            eat(Token.TokenType.SEMI);
            results.add(statement());
        }
        if (currentToken.type == Token.TokenType.ID) {
            this.error();
        }
        return results;
    }

    private AST statement() throws Exception {
        /*
        statement : compound_statement
                  | assignment_statement
                  | empty
         */
        AST node = null;
        if (currentToken.type == Token.TokenType.BEGIN) {
            node = compound_statement();
        }
        else if (currentToken.type == Token.TokenType.ID) {
            node = assignmentStatement();
        }
        else {
            node = empty();
        }
        return node;
    }

    private AST assignmentStatement() throws Exception {
        /*
        assignment_statement : variable ASSIGN expr
         */
        Var left = variable();
        Token token = currentToken;
        eat(Token.TokenType.ASSIGN);
        AST right = expr();
        return new Assign(left,right);
    }

    private Var variable() throws Exception {
        // variable : ID
        Var node = new Var(currentToken);
        eat(Token.TokenType.ID);
        return node;
    }

    private AST empty() {
        // An empty production
        return new NoOp();
    }

    private AST expr() throws Exception {
        // expr : term ((PLUS | MINUS) term)*
        AST node = term();
        while (currentToken.type == Token.TokenType.PLUS || currentToken.type == Token.TokenType.MINUS){
            Token token = currentToken;
            if (token.type == Token.TokenType.PLUS){
                eat(Token.TokenType.PLUS);
            }
            else {
                eat(Token.TokenType.MINUS);
            }
            node = new BinOp(node,token,term());
        }
        return node;
    }

    private AST term() throws Exception {
        // term : factor ((MUL | DIV) factor)*
        AST node = factor();
        while (currentToken.type == Token.TokenType.MUL || currentToken.type == Token.TokenType.DIV) {
            Token token = currentToken;
            if (token.type == Token.TokenType.MUL) {
                eat(Token.TokenType.MUL);
            }
            else {
                eat(Token.TokenType.DIV);
            }
            node = new BinOp(node, token, factor());
        }
        return node;
    }

    private AST factor() throws Exception {
        /*
        factor : PLUS factor
                  | MINUS factor
                  | INTEGER
                  | LPAREN expr RPAREN
                  | variable
         */
        Token token = currentToken;
        AST node = null;
        switch (token.type) {
            case PLUS:
                eat(Token.TokenType.PLUS);
                node = new UnaryOp(token, factor());
                break;
            case MINUS:
                eat(Token.TokenType.MINUS);
                node = new UnaryOp(token, factor());
                break;
            case INTEGER:
                eat(Token.TokenType.INTEGER);
                node = new Num(token);
                break;
            case LPAREN:
                eat(Token.TokenType.LPAREN);
                node = expr();
                eat(Token.TokenType.RPAREN);
                break;
            default:
                node = variable();
        }
        return node;
    }

    public AST parse() throws Exception {
        /*
        program : compound_statement DOT

        compound_statement : BEGIN statement_list END

        statement_list : statement
                       | statement SEMI statement_list

        statement : compound_statement
                  | assignment_statement
                  | empty

        assignment_statement : variable ASSIGN expr

        empty :

        expr: term ((PLUS | MINUS) term)*

        term: factor ((MUL | DIV) factor)*

        factor : PLUS factor
               | MINUS factor
               | INTEGER
               | LPAREN expr RPAREN
               | variable

        variable: ID
         */
        AST node = program();
        if (currentToken.type != Token.TokenType.EOF)
            this.error();
        return node;
    }
}
